/*

<This Java Class is part of the jMusic API version 1.5, March 2004.>

Copyright (C) 2000 Andrew Sorensen & Andrew Brown

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or any
later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/
package jm.audio;

import jm.audio.io.SampleOut;
import jm.music.data.Note;
import jm.music.data.Part;
import jm.music.data.Phrase;
import jm.music.data.Score;

import java.io.*;
import java.util.Enumeration;
import java.util.Stack;

/**
 * The Audio class provides a number of static methods to help pass
 * a jmusic score to the audio architecture and for putting a notes
 * sample information into the correct location in a global audio file.<br><br>
 * WARNING !!!!!!!
 * This class is an absolute disgrace ;)  It works but is very ugly
 * and I can't be bothered to clean it up at the moment. If anyone feels like
 * cleaning it up go for it ;)
 *
 * @author Andrew Sorensen
 * @version 1.0, Sun Feb 25 18:42:43  2001
 */
public final class Audio implements jm.JMC {
    /**
     * Do we want to write the jpf file
     */
    private static boolean JPF = false;
    //Provided so that I can set the number of channels in the addEmUp method
    private static int channels;
    private static int sampleRate;

    /**
     * Makes an array which contains all the notes from all the phrases
     * from all the instruments etc. solely based on start times.
     * This method also writes a jpf file which is a text file containing
     * the names of all the notes corresponding audio files with their
     * start times and lengths.
     *
     * @param score the score to take data from
     */
    public static void processScore(Score score, Instrument[] instList, String fileName) {
        Stack inst = new Stack();
        // add an instrument to avoid errors from no instrument assignment by user
        inst.push(instList[0]);
        for (int i = 0; i < instList.length; i++) {
            if (instList[i] != null) {
                if (!instList[i].getInitialised()) {
                    try {
                        if (instList[i].getInitialised() == false) {
                            instList[i].createChain();
                            instList[i].setInitialised(true);
                        }
                    } catch (AOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        Enumeration enum1 = score.getPartList().elements();
        //set score tempo
        double score_ratio = 60.0 / score.getTempo();
        int partCounter = 0;
        /* Enumerate through all parts */
        while (enum1.hasMoreElements()) {
            Part part = (Part) enum1.nextElement();

            //set part tempo
            double part_ratio = score_ratio;
            if (part.getTempo() > 0.0)
                part_ratio = 60.0 / part.getTempo();

			/* Get the instrument being used for this part */
            if (part.getInstrument() != NO_INSTRUMENT) {
                try {
                    inst.push(instList[part.getInstrument()]);
                } catch (ArrayIndexOutOfBoundsException npe) {
                    System.out.println("jMusic Audio warning: Can't find the instrument number " +
                            part.getInstrument() + " that you have specified for " +
                            "the part named " + part.getTitle() + ".");
                }
            }
            System.out.println("Part " + partCounter++ + " '" + part.getTitle() + "'. ");

			/* Enumerate through all phrases */
            Enumeration enum2 = part.getPhraseList().elements();
            int phraseCounter = 0;
            while (enum2.hasMoreElements()) {
                Phrase phr = (Phrase) enum2.nextElement();
                //get phrase tempo
                double phrase_ratio = part_ratio;
                if (phr.getTempo() > 0.0) {
                    System.out.println("A: " + phrase_ratio);
                    phrase_ratio = 60.0 / phr.getTempo();
                    System.out.println("B: " + phrase_ratio);
                }
				/* Get the instrument being used for this phrase */
                if (phr.getInstrument() != NO_INSTRUMENT) {
                    try {
                        // add phrase instrument to stack
                        inst.push(instList[phr.getInstrument()]);
                    } catch (ArrayIndexOutOfBoundsException npe) {
                        System.out.println("jMusic Audio warning: Can't find the instrument number " +
                                phr.getInstrument() + " that you have specified for" +
                                " the phrase named " + phr.getTitle() + ".");
                    }
                }
                double time = part_ratio * phr.getStartTime(); //start time of phrase
                double ntime = 0.0; //notes distance from phrases start time
                Enumeration enum3 = phr.getNoteList().elements();
                System.out.print("    Phrase " + phraseCounter++ + " '" + phr.getTitle() + "'" +
                        " starting at beat " + phr.getStartTime() + ": ");

                /* Enumerate through all notes */
                int phraseNoteCounter = 0;
                while (enum3.hasMoreElements()) {

                    Note note = (Note) enum3.nextElement();
                    if (note.getFrequency() == (double) REST) { //This a rest ???
                        ntime += phrase_ratio * note.getRhythmValue();
                        continue;
                    }
                    phraseNoteCounter++;
                    if (phraseNoteCounter % 10 == 0) {
                        System.out.print(phraseNoteCounter);
                    } else System.out.print(".");
                    Note new_note = note.copy();
                    //System.out.println("new note pitch = " + new_note.getPitch());
                    new_note.setDuration(phrase_ratio * note.getDuration());
                    new_note.setRhythmValue(phrase_ratio * note.getRhythmValue());
                    Instrument currInst = (Instrument) inst.peek();
                    currInst.setBlock(false);
                    currInst.setFinished(true);
                    currInst.renderNote(new_note, ((double) time + ntime));
                    currInst.setFinished(false);
                    currInst.iterateChain();
                    ntime += phrase_ratio * note.getRhythmValue();
                }

                System.out.println();
                // remove phrase instrument from stack
                if (phr.getInstrument() != NO_INSTRUMENT) inst.pop();
            }

        }
    }

    /**
     * Combine converts the floating point audio file and combines them into an integer file.
     */
    public static void combine(String fileJmp, String tmpFile, String fileOut,
                               boolean deleteFiles, boolean multi) {
        if (multi) {
            Audio.sampleRate = SampleOut.samprate;
            Audio.channels = SampleOut.numofchan;
            System.out.println("Bit Depth: 16" + " Sample rate: " + SampleOut.samprate +
                    " Channels: " + SampleOut.numofchan);
            Audio.addEmUp(tmpFile, fileOut, SampleOut.max);
            return;
        } else {
            int numofdot = 1; //For print outs only

            float max = (float) 0.0; //the largest sample value
            try {
                FileReader fr = new FileReader(fileJmp);
                StreamTokenizer st = new StreamTokenizer(fr);
                RandomAccessFile raf = new RandomAccessFile(tmpFile, "rw");

                double time1 = System.currentTimeMillis();
                for (; ; ) {
                    try {
                        st.nextToken();
                        String fileName = st.sval;
                        if (fileName == null) {
                            //No more tokens
                            break;
                        }
                        st.nextToken();
                        long position = (long) (st.nval * 4);
                        st.nextToken();
                        int length = (int) st.nval;

                        float res = getAudio(fileName, position, length, max, raf);

                        if (max < res) {
                            max = res;
                            System.out.println("Max is smaller: " + max);
                        }
                        if (res < 0 && max < (res * (float) -1.0)) {
                            max = (res * (float) -1.0);
                            System.out.println("MAX is bigger: " + max);
                        }
                        /*
                        if(deleteFiles){
                                if(DEBUG)System.out.println("Deleting " + fileName);
                                File fl = new File(fileName);
                                fl.delete();
                        }
                        */
                        if ((numofdot % 10) == 0) {
                            if (VERBOSE) System.out.print(numofdot);
                        } else {
                            if (VERBOSE) System.out.print(".");
                        }
                        numofdot++;
                        // close random access file
                        raf.close();
                    } catch (EOFException eof) {
                        //This is expected
                        break;
                    } catch (IOException ioe) {
                        ioe.printStackTrace();
                    }
                }
                System.out.print("\n");
                double time2 = System.currentTimeMillis();
                System.out.println("Created tmp file in " + (((time2 - time1)) / 1000.0) + " seconds");

                double now = System.currentTimeMillis();
                //addEmUp(raf, fileOut, max);
                addEmUp(tmpFile, fileOut, max);
                double now2 = System.currentTimeMillis();
                System.out.println("Mixed to a single file in " + (((now2 - now)) / 1000.0) + " seconds");

                if (deleteFiles) {
                    File jmp = new File(fileJmp);
                    File tpm = new File(tmpFile);
                    jmp.delete();
                    tpm.delete();
                }
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }

    private static float getAudio(String fileName, long position, int length,
                                  float max, RandomAccessFile raf) {
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        DataInputStream dis = null;
        try {
            fis = new FileInputStream(fileName);
            bis = new BufferedInputStream(fis, 4096);
            dis = new DataInputStream(bis);
            //Read the files header
            if (dis.readInt() != 0x2E736E64) {
                System.out.println("jMusic SampleIn warning: This file is NOT in the .au/.snd file format");
                return max;
            }
            int offset = dis.readInt();
            int numOfBytes = dis.readInt();
            int format = dis.readInt();
            Audio.sampleRate = dis.readInt();
            Audio.channels = dis.readInt();
            fis.skip(offset - 24); //skip the rest of the header

            //adjust position and length for multiple channels
            position *= (long) channels;
            length *= channels;

        } catch (IOException ioe) {
            ioe.printStackTrace();
        }

        for (; ; ) {
            try {
                raf.seek(position);
                //read in and convert to sample
                float sample = (float) ((float) dis.readShort() / (float) 32767);
                try {//if we can read from the file
                    float d = raf.readFloat();
                    raf.seek(position);
                    position += 4;
                    float tmp = d + sample;
                    if (max < tmp) {
                        max = tmp;
                        System.out.println("MAX small: " + max);
                    }
                    if (tmp < 0 && max < (tmp * (float) -1.0)) {
                        max = (tmp * (float) -1.0);
                        System.out.println("MAX large: " + max);
                    }
                    raf.writeFloat(tmp);
                } catch (EOFException eofe) {
                    //This means that there is nothing to read so we can happily write
                    if (max < sample) max = sample;
                    if (sample < 0 && max < (sample * (float) -1.0)) {
                        max = (sample * (float) -1.0);
                    }
                    raf.writeFloat(sample);
                    position += 4;
                }
            } catch (EOFException eofe) {
                //we've run out of samples to read
                break;
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }

        try {
            dis.close();
            fis.close();
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
        return max;
    }


    public static void addEmUp(String tmpFileName, String fileName, float max) {
        if (VERBOSE) {
            System.out.println("MAX amplitude: " + max);
            System.out.println("Writing .au/.snd file '" + fileName + "' please wait...");
        }
        try {
            FileOutputStream fos = new FileOutputStream(fileName);
            BufferedOutputStream bos = new BufferedOutputStream(fos, 4096);
            DataOutputStream dos = new DataOutputStream(bos);

            File tmpF = new File(tmpFileName);
            FileInputStream fin = new FileInputStream(tmpF);
            BufferedInputStream bin = new BufferedInputStream(fin, 4096);
            DataInputStream dis = new DataInputStream(bin);

            int offset = 28;
            // Modified FP nov 2004
            int numOfBytes = 0; //int numOfBytes = ((int)tmpF.length()/2)+16;
            int format = 3;//6;
            //write header
            dos.writeInt(0x2E736E64); // .snd
            dos.writeInt(offset); //offset from the beginning or the file
            dos.writeInt(numOfBytes); //num of bytes in file (after this)
            dos.writeInt(format); //compression format
            dos.writeInt(Audio.sampleRate); //sampling rate
            dos.writeInt(Audio.channels); //num of channels
            dos.writeInt((short) 0); //add some padding
            //put RandomAccessFile back to the start

            //raf.seek(0);

            double tt = System.currentTimeMillis();
            try {
                for (; ; ) {
                    float samp = dis.readFloat();
                    float outgoing = samp / max;
                    if (outgoing < (float) -1.0 || outgoing > (float) 1.0) {
                        System.out.println("Outgoing= " + outgoing +
                                "  SAMPLE: " + samp + "  MAX: " + max +
                                "  SampleOut.max: " + SampleOut.max);
                    }
                    //dos.writeFloat(outgoing);
                    dos.writeShort((short) (outgoing * 32767));
                    numOfBytes += 2;
                    //System.out.println((short)(dis.readFloat()/max)*32767);
                }
            } catch (EOFException eofe) {
                //expected
                double ttt = System.currentTimeMillis();
                System.out.println("Finished writing the audio file in " + (((ttt - tt)) / 1000.0) + " seconds");
                dos.flush();
                fos.flush();
                bos.flush();
                dos.close();
                fos.close();
                bos.close();
                fin.close();
                bin.close();
                dis.close();
                tmpF.delete();
                // Thanks to Francois Pinot for this work around
                if (tmpF.exists()) { // set to empty (to avoid subsequent overlaying)
                    RandomAccessFile raf = new RandomAccessFile(tmpFileName, "rw");
                    raf.setLength(0);
                    raf.close();
                }
                // Added FP nov 2004
                RandomAccessFile auxraf = new RandomAccessFile(fileName, "rw");
                auxraf.seek(8);
                auxraf.writeInt(numOfBytes);
                auxraf.close();
                // End added FP nov 2004
                return;
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
            System.out.println(ioe);
        }
    }

}
